Micron Document
🎖️GitЯра🎖️


Displaying Raw • Download

core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/GeofenceMonitor.kt 2d20cd8a4708e2ef66e98d5259f3a97ec93f240a (2d20cd8a) Text, 10.13 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.core.data.manager

Tff7b72import T7ee787co.touchlab.kermit.Logger
Tff7b72import T7ee787kotlinx.coroutines.CoroutineScope
Tff7b72import T7ee787kotlinx.coroutines.channels.Channel
Tff7b72import T7ee787kotlinx.coroutines.flow.catch
Tff7b72import T7ee787kotlinx.coroutines.flow.combine
Tff7b72import T7ee787kotlinx.coroutines.launch
Tff7b72import T7ee787org.koin.core.annotation.Named
Tff7b72import T7ee787org.koin.core.annotation.Single
Tff7b72import T7ee787org.meshtastic.core.common.util.nowSeconds
Tff7b72import T7ee787org.meshtastic.core.model.geofence.activeWaypointPackets
Tff7b72import T7ee787org.meshtastic.core.model.geofence.geofencesToMonitor
Tff7b72import T7ee787org.meshtastic.core.model.geofence.toGeofence
Tff7b72import T7ee787org.meshtastic.core.model.util.GeoConstants.DEG_D
Tff7b72import T7ee787org.meshtastic.core.repository.MeshNotificationManager
Tff7b72import T7ee787org.meshtastic.core.repository.NodeManager
Tff7b72import T7ee787org.meshtastic.core.repository.NotificationPrefs
Tff7b72import T7ee787org.meshtastic.core.repository.PacketRepository
Tff7b72import T7ee787org.meshtastic.core.repository.RadioInterfaceService
Tff7b72import T7ee787org.meshtastic.core.repository.RadioSessionContext
Tff7b72import T7ee787org.meshtastic.core.resources.Res
Tff7b72import T7ee787org.meshtastic.core.resources.geofence
Tff7b72import T7ee787org.meshtastic.core.resources.geofence_entered_body
Tff7b72import T7ee787org.meshtastic.core.resources.geofence_entered_title
Tff7b72import T7ee787org.meshtastic.core.resources.geofence_left_body
Tff7b72import T7ee787org.meshtastic.core.resources.geofence_left_title
Tff7b72import T7ee787org.meshtastic.core.resources.getStringSuspend
Tff7b72import T7ee787org.meshtastic.core.resources.unknown_username
Tff7b72import T7ee787org.meshtastic.proto.Position
Tff7b72import T7ee787org.meshtastic.proto.Waypoint
Tff7b72import T7ee787kotlin.concurrent.Volatile

T8b949e/**
* Raises a LOCAL notification when another mesh node's reported position crosses a waypoint's geofence. Evaluation runs
* against OTHER nodes' positions arriving over the mesh — never the device's own location — so this is manual
* point-in-region math, NOT the OS Geofencing API.
*
* Hooked from [MeshDataHandlerImpl.handlePosition]; holds the active geofence-bearing waypoints in memory (refreshed
* from [PacketRepository.getWaypoints], normalised via [activeWaypointPackets] so stale/expired transmissions can't
* fire). By default only waypoints created by THIS device are tracked — geofences are mesh-broadcast, so without that
* filter every receiver in range would alert on the creator's crossings — but the user can opt in to specific foreign
* geofences via [NotificationPrefs.geofenceAlertOptIns]. Crossing state lives in [GeofenceCrossingStore] (in-memory,
* baseline-on-first-sighting).
*
* Received positions are funnelled through a single ordered worker so that two positions for the same node can never be
* evaluated out of order (which would corrupt the inside/outside baseline and fire a spurious or missed alert).
*/
Tf0883e@Single
Tff7b72class T56d364GeofenceMonitorTb4b4b4(
Tff7b72private Tff7b72val Te6edf3packetRepositoryTb4b4b4: Te6edf3LazyTff7b72<Te6edf3PacketRepositoryTff7b72>Tb4b4b4,
Tff7b72private Tff7b72val Te6edf3nodeManagerTb4b4b4: Te6edf3NodeManagerTb4b4b4,
Tff7b72private Tff7b72val Te6edf3serviceNotificationsTb4b4b4: Te6edf3MeshNotificationManagerTb4b4b4,
Tff7b72private Tff7b72val Te6edf3crossingStoreTb4b4b4: Te6edf3GeofenceCrossingStoreTb4b4b4,
Tff7b72private Tff7b72val Te6edf3notificationPrefsTb4b4b4: Te6edf3NotificationPrefsTb4b4b4,
Tff7b72private Tff7b72val Te6edf3radioInterfaceServiceTb4b4b4: Te6edf3RadioInterfaceServiceTb4b4b4,
Tf0883e@NamedTb4b4b4(Ta5d6ff"Ta5d6ffServiceScopeTa5d6ff"Tb4b4b4) Tff7b72private Tff7b72val Te6edf3scopeTb4b4b4: Te6edf3CoroutineScopeTb4b4b4,
Tb4b4b4) Tb4b4b4{

Tff7b72private Tff7b72data Tff7b72class T56d364PositionSampleTb4b4b4(
Tff7b72val Te6edf3nodeNumTb4b4b4: Tffa657IntTb4b4b4,
Tff7b72val Te6edf3latTb4b4b4: Tffa657DoubleTb4b4b4,
Tff7b72val Te6edf3lonTb4b4b4: Tffa657DoubleTb4b4b4,
Tff7b72val Te6edf3sessionTb4b4b4: Te6edf3RadioSessionContext?Tb4b4b4,
Tb4b4b4)

Tf0883e@Volatile Tff7b72private Tff7b72var Te6edf3activeGeofencesTb4b4b4: Te6edf3ListTff7b72<Te6edf3WaypointTff7b72> Tff7b72= Te6edf3emptyListTb4b4b4(Tb4b4b4)

T8b949e// Unbounded so we never drop a position (which could swallow a real crossing); positions arrive infrequently.
Tff7b72private Tff7b72val Te6edf3samples Tff7b72= Te6edf3ChannelTff7b72<Te6edf3PositionSampleTff7b72>Tb4b4b4(Te6edf3ChannelTb4b4b4.Te6edf3UNLIMITEDTb4b4b4)

Tff7b72init Tb4b4b4{
T8b949e// Single serial consumer → evaluations happen in arrival order, and we launch ONE coroutine, not one per
T8b949e// received position.
Te6edf3scopeTb4b4b4.Te6edf3launch Tb4b4b4{
Tff7b72for Tb4b4b4(Te6edf3sample Tff7b72in Te6edf3samplesTb4b4b4) Tb4b4b4{
Tff7b72try Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3sampleTb4b4b4.Te6edf3session Tff7b72=Tff7b72= Tff7b72nullTb4b4b4) Tb4b4b4{
Te6edf3evaluateTb4b4b4(Te6edf3sampleTb4b4b4.Te6edf3nodeNumTb4b4b4, Te6edf3sampleTb4b4b4.Te6edf3latTb4b4b4, Te6edf3sampleTb4b4b4.Te6edf3lonTb4b4b4)
Tb4b4b4} Tff7b72else Tb4b4b4{
Te6edf3radioInterfaceServiceTb4b4b4.Te6edf3runWhileSessionActiveTb4b4b4(Te6edf3sampleTb4b4b4.Te6edf3sessionTb4b4b4) Tb4b4b4{
Te6edf3evaluateTb4b4b4(Te6edf3sampleTb4b4b4.Te6edf3nodeNumTb4b4b4, Te6edf3sampleTb4b4b4.Te6edf3latTb4b4b4, Te6edf3sampleTb4b4b4.Te6edf3lonTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4} Tff7b72catch Tb4b4b4(Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4) Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
T8b949e// Isolate per-sample failures: an unexpected throw must not kill the sole consumer and silently
T8b949e// stop geofence tracking for the rest of the session.
Te6edf3LoggerTb4b4b4.Te6edf3eTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffGeofence evaluation failed for node Tffd700${Te6edf3sampleTb4b4b4.Te6edf3nodeNumTffd700}Ta5d6ff; skipping sampleTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
T8b949e// A bad emission must not tear down the snapshot for the rest of the session. Combined with myNodeNum (so the
T8b949e// creator-only filter recomputes once our node number is known — waypoints can arrive before we're connected)
T8b949e// and the opt-in set (so subscribing to a foreign geofence takes effect immediately).
Te6edf3scopeTb4b4b4.Te6edf3launch Tb4b4b4{
Te6edf3combineTb4b4b4(
Te6edf3packetRepositoryTb4b4b4.Te6edf3valueTb4b4b4.Te6edf3getWaypointsTb4b4b4(Tb4b4b4)Tb4b4b4,
Te6edf3nodeManagerTb4b4b4.Te6edf3myNodeNumTb4b4b4,
Te6edf3notificationPrefsTb4b4b4.Te6edf3geofenceAlertOptInsTb4b4b4,
Tb4b4b4) Tb4b4b4{ Te6edf3packetsTb4b4b4, Te6edf3myNodeNumTb4b4b4, Te6edf3optIns Tff7b72-Tff7b72>
Te6edf3packetsTb4b4b4.Te6edf3activeWaypointPacketsTb4b4b4(Te6edf3nowSecondsTb4b4b4)Tb4b4b4.Te6edf3valuesTb4b4b4.Te6edf3geofencesToMonitorTb4b4b4(Te6edf3myNodeNumTb4b4b4, Te6edf3optInsTb4b4b4)
Tb4b4b4}
Tb4b4b4.Te6edf3catch Tb4b4b4{ Te6edf3LoggerTb4b4b4.Te6edf3eTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffGeofence waypoint stream failed; geofence tracking pausedTa5d6ff" Tb4b4b4} Tb4b4b4}
Tb4b4b4.Te6edf3collect Tb4b4b4{ Te6edf3active Tff7b72-Tff7b72>
Te6edf3activeGeofences Tff7b72= Te6edf3active
Te6edf3crossingStoreTb4b4b4.Te6edf3retainOnlyTb4b4b4(Te6edf3activeTb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3id Tb4b4b4}Tb4b4b4.Te6edf3toSetTb4b4b4(Tb4b4b4)Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

T8b949e/** Evaluate a received node position against every active geofence. [nodeNum] is the position's sender. */
Tff7b72fun Td2a8ffonPositionReceivedTb4b4b4(Te6edf3nodeNumTb4b4b4: Tffa657IntTb4b4b4, Te6edf3myNodeNumTb4b4b4: Tffa657IntTb4b4b4, Te6edf3positionTb4b4b4: Te6edf3PositionTb4b4b4, Te6edf3sessionTb4b4b4: Te6edf3RadioSessionContext? Tff7b72= Tff7b72nullTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3latI Tff7b72= Te6edf3positionTb4b4b4.Te6edf3latitude_i Tff7b72?: T79c0ff0
Tff7b72val Te6edf3lonI Tff7b72= Te6edf3positionTb4b4b4.Te6edf3longitude_i Tff7b72?: T79c0ff0
Tff7b72val Te6edf3lat Tff7b72= Te6edf3latI Tff7b72* Te6edf3DEG_D
Tff7b72val Te6edf3lon Tff7b72= Te6edf3lonI Tff7b72* Te6edf3DEG_D
T8b949e// Skip our own position, a node with no fix (don't false-cross at 0,0), out-of-range/garbage coordinates, or
T8b949e// when nothing is geofenced.
Tff7b72val Te6edf3skip Tff7b72=
Te6edf3nodeNum Tff7b72=Tff7b72= Te6edf3myNodeNum Tff7b72|Tff7b72|
Tb4b4b4(Te6edf3latI Tff7b72=Tff7b72= T79c0ff0 Tff7b72&Tff7b72& Te6edf3lonI Tff7b72=Tff7b72= T79c0ff0Tb4b4b4) Tff7b72|Tff7b72|
Te6edf3lat Tff7b72!in Te6edf3MIN_LATITUDETb4b4b4.Tb4b4b4.Te6edf3MAX_LATITUDE Tff7b72|Tff7b72|
Te6edf3lon Tff7b72!in Te6edf3MIN_LONGITUDETb4b4b4.Tb4b4b4.Te6edf3MAX_LONGITUDE Tff7b72|Tff7b72|
Te6edf3activeGeofencesTb4b4b4.Te6edf3isEmptyTb4b4b4(Tb4b4b4)
Tff7b72if Tb4b4b4(Te6edf3skipTb4b4b4) Tff7b72return
Te6edf3samplesTb4b4b4.Te6edf3trySendTb4b4b4(Te6edf3PositionSampleTb4b4b4(Te6edf3nodeNumTb4b4b4, Te6edf3latTb4b4b4, Te6edf3lonTb4b4b4, Te6edf3sessionTb4b4b4)Tb4b4b4)
Tb4b4b4}

Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffevaluateTb4b4b4(Te6edf3nodeNumTb4b4b4: Tffa657IntTb4b4b4, Te6edf3latTb4b4b4: Tffa657DoubleTb4b4b4, Te6edf3lonTb4b4b4: Tffa657DoubleTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3now Tff7b72= Te6edf3nowSeconds
Tff7b72for Tb4b4b4(Te6edf3waypoint Tff7b72in Te6edf3activeGeofencesTb4b4b4) Tb4b4b4{
T8b949e// Re-check expiry per evaluation: the snapshot is only recomputed on a new waypoint emission, so a
T8b949e// waypoint that expired since the last emission must not keep firing.
Tff7b72if Tb4b4b4(Te6edf3waypointTb4b4b4.Te6edf3expire Tff7b72=Tff7b72= T79c0ff0 Tff7b72|Tff7b72| Te6edf3waypointTb4b4b4.Te6edf3expireTb4b4b4.Te6edf3toLongTb4b4b4(Tb4b4b4) Tff7b72> Te6edf3nowTb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3geofence Tff7b72= Te6edf3waypointTb4b4b4.Te6edf3toGeofenceTb4b4b4(Tb4b4b4) Tff7b72?: Tff7b72continue
Tff7b72when Tb4b4b4(Te6edf3crossingStoreTb4b4b4.Te6edf3updateTb4b4b4(Te6edf3waypointTb4b4b4.Te6edf3idTb4b4b4, Te6edf3nodeNumTb4b4b4, Te6edf3geofenceTb4b4b4.Te6edf3containsTb4b4b4(Te6edf3latTb4b4b4, Te6edf3lonTb4b4b4)Tb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3GeofenceTransitionTb4b4b4.Te6edf3ENTERED Tff7b72-Tff7b72> Tff7b72if Tb4b4b4(Te6edf3waypointTb4b4b4.Te6edf3notify_on_enterTb4b4b4) Te6edf3notifyCrossingTb4b4b4(Te6edf3waypointTb4b4b4, Te6edf3nodeNumTb4b4b4, Tff7b72trueTb4b4b4)

Te6edf3GeofenceTransitionTb4b4b4.Te6edf3EXITED Tff7b72-Tff7b72> Tff7b72if Tb4b4b4(Te6edf3waypointTb4b4b4.Te6edf3notify_on_exitTb4b4b4) Te6edf3notifyCrossingTb4b4b4(Te6edf3waypointTb4b4b4, Te6edf3nodeNumTb4b4b4, Tff7b72falseTb4b4b4)

Te6edf3GeofenceTransitionTb4b4b4.Te6edf3BASELINETb4b4b4,
Te6edf3GeofenceTransitionTb4b4b4.Te6edf3UNCHANGEDTb4b4b4,
Tff7b72-Tff7b72> Tffa657Unit
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72suspend Tff7b72fun Td2a8ffnotifyCrossingTb4b4b4(Te6edf3waypointTb4b4b4: Te6edf3WaypointTb4b4b4, Te6edf3nodeNumTb4b4b4: Tffa657IntTb4b4b4, Te6edf3enteredTb4b4b4: Tffa657BooleanTb4b4b4) Tb4b4b4{
T8b949e// Favorites gate is receiver-local and resolved only on a real transition.
Tff7b72if Tb4b4b4(Te6edf3waypointTb4b4b4.Te6edf3notify_favorites_only Tff7b72&Tff7b72& Te6edf3nodeManagerTb4b4b4.Te6edf3nodeDBbyNodeNumTff7b72[Te6edf3nodeNumTff7b72]Tff7b72?.Te6edf3isFavorite Tff7b72!Tff7b72= Tff7b72trueTb4b4b4) Tff7b72return

Tff7b72try Tb4b4b4{
Tff7b72val Te6edf3nodeName Tff7b72=
Te6edf3nodeManagerTb4b4b4.Te6edf3nodeDBbyNodeNumTff7b72[Te6edf3nodeNumTff7b72]Tff7b72?.Te6edf3userTff7b72?.Te6edf3long_nameTff7b72?.Te6edf3takeIf Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3isNotBlankTb4b4b4(Tb4b4b4) Tb4b4b4}
Tff7b72?: Te6edf3getStringSuspendTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3unknown_usernameTb4b4b4)
Tff7b72val Te6edf3waypointName Tff7b72= Te6edf3waypointTb4b4b4.Te6edf3nameTb4b4b4.Te6edf3takeIf Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3isNotBlankTb4b4b4(Tb4b4b4) Tb4b4b4} Tff7b72?: Te6edf3getStringSuspendTb4b4b4(Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3geofenceTb4b4b4)
Tff7b72val Te6edf3title Tff7b72=
Te6edf3getStringSuspendTb4b4b4(
Tff7b72if Tb4b4b4(Te6edf3enteredTb4b4b4) Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3geofence_entered_title Tff7b72else Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3geofence_left_titleTb4b4b4,
Te6edf3waypointNameTb4b4b4,
Tb4b4b4)
Tff7b72val Te6edf3body Tff7b72=
Te6edf3getStringSuspendTb4b4b4(
Tff7b72if Tb4b4b4(Te6edf3enteredTb4b4b4) Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3geofence_entered_body Tff7b72else Te6edf3ResTb4b4b4.Te6edf3stringTb4b4b4.Te6edf3geofence_left_bodyTb4b4b4,
Te6edf3nodeNameTb4b4b4,
Te6edf3waypointNameTb4b4b4,
Tb4b4b4)
T8b949e// Reuse the waypoint notification channel + map?waypointId= deep link. A distinct contactKey per
T8b949e// (waypoint, node) gives each crossing its own notification id (matches the Apple reference).
Te6edf3serviceNotificationsTb4b4b4.Te6edf3updateWaypointNotificationTb4b4b4(
Te6edf3contactKey Tff7b72= Ta5d6ff"Ta5d6ffgeofence:Tffd700${Te6edf3waypointTb4b4b4.Te6edf3idTffd700}Ta5d6ff:Tffd700$Te6edf3nodeNumTa5d6ff"Tb4b4b4,
Te6edf3name Tff7b72= Te6edf3titleTb4b4b4,
Te6edf3message Tff7b72= Te6edf3bodyTb4b4b4,
Te6edf3waypointId Tff7b72= Te6edf3waypointTb4b4b4.Te6edf3idTb4b4b4,
Tb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4) Te6edf3eTb4b4b4: Te6edf3ExceptionTb4b4b4) Tb4b4b4{
T8b949e// A string-resource or notification failure must not take down the evaluation worker.
Te6edf3LoggerTb4b4b4.Te6edf3eTb4b4b4(Te6edf3eTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffFailed to raise geofence crossing notification for waypoint Tffd700${Te6edf3waypointTb4b4b4.Te6edf3idTffd700}Ta5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72companion Tff7b72object Tb4b4b4{
Tff7b72const Tff7b72val Te6edf3MIN_LATITUDE Tff7b72= Tff7b72-T79c0ff9T79c0ff0.0
Tff7b72const Tff7b72val Te6edf3MAX_LATITUDE Tff7b72= T79c0ff9T79c0ff0.0
Tff7b72const Tff7b72val Te6edf3MIN_LONGITUDE Tff7b72= Tff7b72-T79c0ff1T79c0ff8T79c0ff0.0
Tff7b72const Tff7b72val Te6edf3MAX_LONGITUDE Tff7b72= T79c0ff1T79c0ff8T79c0ff0.0
Tb4b4b4}
Tb4b4b4}

Served by rngit 1.5.0 - Generated in 0.06s